// ==UserScript== // @name 🔐 密码填充 // @namespace https://ez118.github.io/ // @version 0.2.7 // @description 为Via设计的第三方密码自动保存/填充工具,支持管理与导出密码 // @author ZZY_WISU // @match *://*/* // @license GPLv3 // @run-at document-end // @grant GM_registerMenuCommand // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // ==/UserScript== /* =====[ 变量存储 ]===== */ const ICONS = { 'del': '' }; var savedAccount = []; /* ====================== */ function Toast(text) { try{ if (typeof(window.via) == "object") window.via.toast(text); else if (typeof(window.mbrowser) == "object") window.mbrowser.showToast(text); else alert(text); }catch{ alert(text); } } function hash(str) { let hash = 5381; for (let i = 0; i < str.length; i++) { hash = (hash * 33) ^ str.charCodeAt(i); } return hash >>> 0; } function downloadFile(fileName, text) { // 下载指定内容的文件 const url = window.URL || window.webkitURL || window; const blob = new Blob([text]); const saveLink = document.createElementNS('http://www.w3.org/1999/xhtml', 'a'); saveLink.href = url.createObjectURL(blob); saveLink.download = fileName; saveLink.click(); } function getHost() { // 获取当网站域名 return window.location.host; } function findByKeyValue(array, key, value) { // 在JSON中,以键值匹配项 return array.findIndex(item => item[key] === value); } function triggerFileSelect(callback) { // 打开文件选择框 const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.csv'; fileInput.multiple = false; // 监听文件选择事件 fileInput.addEventListener('change', (event) => { const files = event.target.files; if (files.length > 0) { callback(files[0]); } else { // return null; } }); fileInput.click(); } function exportAccountData(){ // 导出保存的账号 let csvText = "name,url,username,password,note\n"; let fileName = "密码填充_" + hash(csvText) % 1e8 + ".csv"; savedAccount.forEach((item, index) => { csvText += `${item.host},https://${item.host}/,${item.username},${item.password},\n`; }); downloadFile(fileName, csvText); } function importAccountData(){ // 导入来自电脑浏览器的账号 alert("【导入账号】 即将弹出文件选择,请选择 与Chrome/Firefox/Edge浏览器兼容 的CSV格式文件。"); triggerFileSelect((file) => { try{ const reader = new FileReader(); reader.onload = function(e) { Toast("文件读取成功,正在导入..."); const text = e.target.result; const lines = text.split('\n'); var newDataList = []; // 遍历每一行 lines.forEach((line, index) => { // 第一行是 表头,直接跳过 if(index == 0 || line.length <= 6 || line.length > 512) { return; } // 取得每一项的值 const item = line.split(","); let username = item[2]; let password = item[3]; let host = item[1]; // 只留网址中的域名部分 if (host.includes("://")) { host = host.split("/")[2]; } // 值缺失,则跳过 if(!username || !password || !host) { return; } // 向新列表插入项 newDataList.push({ "id": hash(host + username + password).toString(), "host": host, "username": username, "password": password }); }); savedAccount = savedAccount.concat(newDataList); // 账号去重 const uniqueDataList = savedAccount.reduce((accumulator, current) => { const exists = accumulator.some(item => item.id === current.id); if (!exists) { accumulator.push(current); } return accumulator; }, []); savedAccount = uniqueDataList; GM_setValue('savedAccount', savedAccount); Toast("账号已导入合并,请刷新以查看更改"); }; reader.readAsText(file); } catch(e) { Toast("账号导入失败"); console.error("【错误】账号导入失败,如果是脚本程序错误,请尽快向作者反馈并提供报错内容。 \n", e); } }) } function isLoginPage() { // 检查当前网页是否是满足要求的登录页面 let forms = document.getElementsByTagName("form"); let isLogin = false; let formPosition = {x: 0, y: 0}; let formobj = null; Array.prototype.forEach.call(forms, (form) => { let hasTextInput = false; let hasPasswordInput = false; // 获取所有 input 元素 let inputs = form.getElementsByTagName("input"); // 检查每个 input 的类型 Array.prototype.forEach.call(inputs, (input) => { if (input.type === "text" || input.type === "email") { hasTextInput = true; } else if (input.type === "password") { hasPasswordInput = true; } }); // 如果同时存在 text 和 password 类型的输入框,认为是登录页面 if (hasTextInput && hasPasswordInput) { isLogin = true; let rectData = form.getClientRects()[0]; formPosition.x = rectData.left + rectData.width / 2 - 90; formPosition.y = rectData.top + rectData.height - 15; formobj = form; } }); return { isLogin, x: formPosition.x, y: formPosition.y, obj: formobj }; } function getFormData(ele){ // 获取当前页面内登录框的内容(ele传入登录框所在form元素的对象) let inputs = ele.getElementsByTagName("input"); let usr = null; let psw = null; // 检查每个 input 的类型 Array.prototype.forEach.call(inputs, (input) => { if ((input.type === "text" || input.type === "email") && !usr) { usr = input; } else if (input.type === "password" && !psw) { psw = input; } }); return {password: psw.value, username: usr.value, psw: psw, usr: usr}; } function showPswMgr() { // 显示账户管理界面 if (document.getElementById("userscript-pswmgrDlg")) { return; } let newAccountList = savedAccount.slice(); // 不直接引用 let origAccountList = savedAccount.slice(); // 创建元素、设置属性 const optDlg = document.createElement('div'); optDlg.className = 'userscript-pswmgrDlg'; optDlg.id = 'userscript-pswmgrDlg'; optDlg.style.display = 'none'; document.body.appendChild(optDlg); // 循环输出账户列表的html let listHtml = newAccountList.map(item => `

${item.username} (${item.host})

${ICONS.del}

`).join(''); // 显示管理对话框html框架 optDlg.innerHTML = `

管理

已保存的账户:

${listHtml}
`; optDlg.style.display = 'block'; // 绑定全局点击事件 document.addEventListener('click', onClick); // 对全局点击事件进行判断,判断点击事件作用对象(ChatGPT的主意,实现方式奇怪,但兼容性变强了) function onClick(e) { if (e.target.parentElement.className == "item-delbtn" || e.target.parentElement.parentElement.className == "item-delbtn") { let btnEle = (e.target.parentElement.className == "item-delbtn") ? e.target.parentElement : e.target.parentElement.parentElement; console.log(btnEle) const acid = btnEle.getAttribute("acid"); const index = findByKeyValue(newAccountList, 'id', acid); if (index !== -1) { newAccountList.splice(index, 1); btnEle.parentElement.remove(); } } if (e.target.id === 'userscript-cancelBtn') { newAccountList = origAccountList; // 恢复原始账户列表 closeDialog(); } if (e.target.id === 'userscript-saveBtn') { savedAccount = newAccountList; // 更新全局账户列表 GM_setValue('savedAccount', savedAccount); Toast("已保存,刷新页面以应用更改"); closeDialog(); } if (e.target.id === 'userscript-exportBtn') { exportAccountData(); Toast("即将导出为csv文件,请注意下载"); } if (e.target.id === 'userscript-importBtn') { importAccountData(); } } // 关闭窗口 function closeDialog() { const optDlg = document.getElementById("userscript-pswmgrDlg"); optDlg.style.display = 'none'; setTimeout(() => { optDlg.remove(); document.removeEventListener('click', onClick); }, 110); } } function initEle(form, cx, cy) { // 创建搜索栏元素并添加到页面 const quickFill = document.createElement('div'); quickFill.className = 'userscript-quickFill'; quickFill.id = 'userscript-quickFill'; document.body.appendChild(quickFill); let html = ''; const host = getHost(); savedAccount.forEach(item => { if (item.host === host) { html += `
${item.username}
`; } }); // 设定快速填充栏HTML内容 quickFill.innerHTML = `  保存的密码: ${html}
[隐藏]
`; // 设置快速填充栏位置 quickFill.style.left = `${cx}px`; quickFill.style.top = `${cy}px`; // 选择保存过的第一个账号,自动填充到网页 const formdata = getFormData(form); let dataindex = findByKeyValue(savedAccount, 'host', host); if (dataindex !== -1) { formdata.psw.value = savedAccount[dataindex].password; formdata.usr.value = savedAccount[dataindex].username; } // 添加点击事件监听器 quickFill.addEventListener('click', function (e) { if (e.target.matches('.item')) { const acid = e.target.getAttribute("acid"); let dataindex = findByKeyValue(savedAccount, 'id', acid); formdata.psw.value = savedAccount[dataindex].password; formdata.usr.value = savedAccount[dataindex].username; } if (e.target.matches('.hideBtn')) { quickFill.style.display = 'none'; } }); } function init() { let judgeRes = isLoginPage(); if (judgeRes.isLogin) { /* 存储初始化 */ console.log("【提示】检测到登录页面"); initEle(judgeRes.obj, judgeRes.x, judgeRes.y); judgeRes.obj.addEventListener('submit', function (e) { // 获取表单输入内容 const formdata = getFormData(judgeRes.obj); const newdata = { "id": hash(getHost() + formdata.username + formdata.password).toString(), "host": getHost(), "username": formdata.username, "password": formdata.password }; // 检查是否数据重复 const oldidx = findByKeyValue(savedAccount, "host", newdata.host); if (oldidx !== -1 && savedAccount[oldidx] && savedAccount[oldidx].id === newdata.id) { return; } // 如果不是重复账号,则询问是否保存 let res = window.confirm("【询问】是否保存账号?"); if (res) { // 保存账户 savedAccount.push(newdata); GM_setValue('savedAccount', savedAccount); Toast("账号已保存!"); } }); } } /* =====[ 菜单注册 ]===== */ var menu_mgr = GM_registerMenuCommand('⚙️ 管理密码', function () { showPswMgr(); }, 'o'); (function () { 'use strict'; if(GM_getValue('savedAccount') == null || GM_getValue('savedAccount') == "" || GM_getValue('savedAccount') == undefined){ GM_setValue('savedAccount', savedAccount); } else { savedAccount = GM_getValue('savedAccount'); } var websiteThemeColor = "#FFFFFFEE"; var websiteFontColor = "#000"; GM_addStyle(` body{ -webkit-appearance:none!important; } .userscript-quickFill{ user-select:none; background-color:${websiteThemeColor}; color:${websiteFontColor}; border:1px solid #99999999; padding:5px; font-size:12px; line-height:20px; width:180px; height:fit-content; position:absolute; display:flex; flex-direction:column; overflow:hidden auto; box-sizing:border-box; z-index:100000; font-family:"Hiragino Sans GB","Microsoft YaHei","WenQuanYi Micro Hei",sans-serif; border-radius:10px; box-shadow:0px 0px 5px #666; } .userscript-quickFill>.item{ margin:1px 0px; border-radius:8px; padding:5px 9px; width:100%; flex-basis:fit-content; flex-shrink:0; cursor:pointer; background-color:transparent; box-sizing:border-box } .userscript-quickFill>.item:hover{ background-color:rgba(128, 128, 128, 0.2); } .userscript-quickFill>.hideBtn{ margin:1px 0px; padding:5px 9px; width:100%; flex-basis:fit-content; flex-shrink:0; color:${websiteFontColor}; opacity:0.6; font-size:12px; font-weight:bold; box-sizing:border-box; cursor:pointer; } .userscript-pswmgrDlg{ user-select:none; background-color:${websiteThemeColor}; color:${websiteFontColor}; border:1px solid #99999999; position:fixed; top:50%; height:fit-content; left:50%; transform:translateX(-50%) translateY(-50%); width:92vw; max-width:300px; max-height:92vh; padding:15px; border-radius:15px; box-sizing:initial; z-index:100000; box-shadow:0 1px 10px #00000088; font-family:"Hiragino Sans GB","Microsoft YaHei","WenQuanYi Micro Hei",sans-serif; } .userscript-pswmgrDlg .ctrlbtn{ border:none; background-color:transparent; padding:8px; margin:0; color:#6d7fb4; cursor:pointer; overflow:hidden; } .userscript-pswmgrDlg h3{ margin:5px; margin-bottom:15px; font-size:24px; } .userscript-pswmgrDlg .subtitle{ margin:5px 1px; font-size:16px; font-weight:400; } .userscript-pswmgrDlg .list-item{ width:calc(100% - 10px); padding:10px 5px; margin:0; display:flex; flex-direction:row; vertical-align:middle; box-sizing:initial; } .userscript-pswmgrDlg .list-item:hover{ background-color:#55555555; } .userscript-pswmgrDlg .list-item>p{ padding:0; margin:0; font-size:16px; } .userscript-pswmgrDlg .list-item>.item-title{ flex-grow:1; margin-left:5px; } .userscript-pswmgrDlg .list-item>.item-delbtn{ cursor:pointer; width:25px; } .userscript-pswmgrDlg .list-item>.item-delbtn svg{ fill:${websiteFontColor}; height:100%; min-height:16px; } `); init(); setTimeout(function () { if (document.querySelectorAll(".userscript-quickFill").length === 0) { init(); } }, 1000); })();